A Hands-on Guide: Building and Deploying Containerized web application with ZK and AWS Lightsail
Hawk Chen, Engineer, Potix Corporation
May. 31, 2023
ZK 10FL
Overview
In this article, I will delve into the process of building and deploying a cloud-based web application using the ZK 10 and AWS Lightsail. ZK 10, currently under development, introduces an exciting feature, stateless components, designed to aid developers in creating efficient, cloud-based web applications. This makes enhancing scalability, high availability, resource management, and auto-provisioning much easier.
This article will cover stateless components' basics, configuration, and usage. We will also guide you through the steps of deploying your application using Docker containers on AWS Lightsail.
Basic Concept of Stateless Components
The Differences Between Stateless and Stateful Components
ZK is a server-centric framework, and the standard UI components we have been offering are stateful(ZK 9 and before). Every UI component produces a Java object and a JavaScript widget that maintain their state in synchrony. The desktop and its component tree are stored in an HTTP session. This model, while powerful, required memory on the server to maintain the state of each Java object.
However, with the advent of ZK 10, a new type of component is introduced: the stateless component. In contrast to standard components, stateless components do not have a corresponding Java object keeping their state and no persistent desktop. Instead, only JavaScript widgets maintain the state of the UI. This means ZK no longer requires requests in the same session to be sent to the same server. They can be processed from any clustered node so that you can fully leverage cloud resources in a more dynamic and resilient manner and achieve autoscaling and high availability much easier. Another significant advantage of this architecture is that stateless components do not consume server memory, as no Java objects hold their state.
The screenshot below demonstrates that no component (Java) objects are created when using stateless components:
Heap dump in VisualVM
Configure Dispatcher Richlet Filter
To start working with stateless components, you'll need to configure the DispatcherRichletFilter
in your web.xml
file. The DispatcherRichletFilter is a core role in handling requests in ZK 10. It maps incoming HTTP requests to methods in a StatelessRichlet
that are annotated with the @RichletMapping
, based on the URL pattern defined in the annotation.
Here is a basic configuration of the DispatcherRichletFilter:
<filter>
<filter-name>DispatcherRichletFilter</filter-name>
<filter-class>org.zkoss.stateless.ui.http.DispatcherRichletFilter</filter-class>
<init-param>
<param-name>basePackages</param-name>
<param-value>org.zkoss.stateless.demo</param-value>
</init-param>
<init-param>
<param-name>cloudMode</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>DispatcherRichletFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- The
basePackages
parameter in the configuration specifies the packages that the filter should scan for@RichletMapping
. This is a required parameter that you should specify according to your conditions.
Building UI with Stateless Components
In ZK 10, to build a UI with stateless components, one needs to create a class that implements the StatelessRichlet
interface and designates its access URL. This can be done using the @RichletMapping
on both the class and method to define the access URL by concatenating class and method annotation values:
[CLASS_VALUE][METHOD_VALUE]
@RichletMapping("/shoppingCart")
public class ShoppingCartRichlet implements StatelessRichlet {
@RichletMapping("")
public List<IComponent> index() {
//... your code here
}
Based on the code above, the access URL is /shoppingcart
The new stateless component API introduces a fluent API style, where each class begins with an uppercase "I", indicating "Immutable". Below is an example of how to build a UI with this new API.
@RichletMapping("")
public List<IComponent> index() {
return asList(
IStyle.ofSrc(DEMO_CSS),
IVlayout.of(
renderShoppingBag(),
Boilerplate.ORDER_TEMPLATE
)
);
}
private IVlayout renderShoppingBag() {
final String orderId = Helper.nextUuid();
return IVlayout.of(
ILabel.of("Shopping Cart").withSclass("title"),
renderOrderButtons(orderId),
IGrid.ofId("shoppingBag").withHflex("1")
.withEmptyMessage("please add items.")
.withColumns(Boilerplate.SHOPPING_BAG_COLUMN_TEMPLATE)
.withRows(renderShoppingBagItems(orderId))
)
.withSclass("shoppingBag");
}
of()
is used to append child componentswithAttr()
is used to set a component's attributes. For example, withId() is used to set the ID of the component.
Action Handling
In the new ZK 10's stateless component model, the traditional "event listeners" from ZK 9 are replaced with "action handlers". You can set an action handler with the withAction()
, such as IButton.of("add item +").withAction(this::addItem)
.
To make a method an action handler, you apply the @Action
with an event type to listen to. An example is shown below:
@Action(type = Events.ON_CLICK)
public void addItem() {
//... your code here
}
In the code snippet above, the @Action(type = Events.ON_CLICK)
denotes that the method addItem() is an action handler for the ON_CLICK event type. When a click event occurs, this addItem() method will be invoked to handle the action.
Obtain Component State
Given the stateless nature of ZK 10's components, obtaining the component's state from the server using a getter is no longer possible. Instead, you need to obtain the states from a browser. ZK 10 provides the @ActionVariable
to access component states sent from a browser.
@Action(type = Events.ON_CLICK)
public void addItem(@ActionVariable(targetId = ActionTarget.SELF, field = "id") String id) {
// your code here
}
ActionTarget.SELF
refers to the event target, which in this case is the “add item” button. This could also beActionTarget.PARENT
,ActionTarget.FIRST_CHILD
,ActionTarget.LAST_CHILD
, and so forth.
Here is another example:
@Action(type = Events.ON_CHANGE)
public void changeQuantity(Self self,
InputData quantityData,
@ActionVariable(targetId = ActionTarget.NEXT_SIBLING) Integer price,
@ActionVariable(targetId = ActionTarget.PARENT, field = "id") String id) {
// your code here
}
self
refers to the locator of the component itselfquantityData
represents input data caused by a user's input something at the client.
Update Component State
Since the server no longer holds components in ZK 10, a UiAgent
is used to control the client-side widgets and allow them to update themselves.
@Action(type = Events.ON_CLICK)
public void deleteItem(Self self, @ActionVariable(targetId = ActionTarget.PARENT, field = "id") String id) {
orderService.delete(parseItemId(id));
UiAgent.getCurrent().remove(Locator.ofId(id));
// your code here
}
- The
Locator.ofId()
function returns a locator to the given ID of an `IComponent`. This is used to specify the component to be removed by the `UiAgent`.
Deploying the App
Now that we have a simple web application created using ZK 10 stateless components, we can either deploy it to a standalone web server just like we used to do or deploy it to a cloud environment so that we can fully leverage the flexible scaling and auto-provisioning features.
You are free to choose the cloud platform you wish to deploy your app to. In this article, we will take AWS Lightsail as an example to demonstrate how the process is done.
AWS Lightsail Getting Started
Amazon Web Services Lightsail is a simple yet powerful platform that allows you to build, deploy, and manage your web applications effortlessly. It's designed to be easy to use, and it's ideal for beginners who want to quickly get a project off the ground. For a more detailed introduction and guide to using Lightsail, I recommend referencing the official AWS tutorial: "a Container Web App on Amazon Lightsail".
Common uses for Lightsail include:
- Web Application Hosting
- Dev/Test Environments
- Microservices
- Content Delivery and Media Serving
Install the AWS CLI
To interact with AWS services, you'll need the AWS Command Line Interface (CLI) installed on your local machine. Follow the official instructions provided to get started.
Configure AWS CLI Credentials
After installing the AWS CLI, the next step is to configure your credentials. This will allow the CLI to authenticate your requests to AWS. Again, follow the official guide to set up your credentials. Details will not be covered here as the AWS guide provides comprehensive instructions.
Prepare Docker Image
To get your web application running on Lightsail, you'll need to package it as a Docker image. We assume you have Docker Desktop installed on your machine and are familiar with basic Docker commands. In this guide, our Dockerfile will be based on openjdk:8-jdk
, and we'll also install postgresql-12
and Tomcat 8 to run the war file.
Steps to run a container service
- Create a Container Service
- Push a Local Image
- Deploy That Image to the Container Service
Create a Container Service
An Amazon Lightsail container service is a compute resource where you can deploy your Docker images. Think of your Lightsail container service as a computing environment that allows you to run containers on AWS infrastructure using images that you create.
To get started with Lightsail container services, you will need to install the AWS CLI Lightsail Extension. Follow the official instructions provided by Amazon.
A container service is comprised of compute nodes, a TLS certificate, a DNS domain name, and an optional load balancer. For simplicity, a shell script named create-container.sh
is provided to create a container service. After executing the script, wait for a while until the container service's status changes to READY.
Push a Local Image
Amazon Lightsail supports the deployment of containers from various public container image repositories, such as Docker Hub, Amazon ECR Public Gallery, and even your local machine.
To begin, build a Docker image using docker-build.sh
. You can then test the Docker image locally by running docker-up.sh
. To push the image from your local machine to Lightsail, execute push-container.sh
. The pushed image, in this case, named 'shoppingcart', will be stored on Amazon Lightsail and can be verified under the "Image" tab in your container service.
Deploy That Image to the Container Service
After the image is pushed, it needs to be deployed to the container service to run. This can be achieved by executing the script deploy-container.sh
or through the Lightsail console.
Once the deployment process is complete and the container service's status changes to RUNNING, you can visit the public domain assigned to your service to access the application.
Change Capacity to Serve More Requests
After deploying to a container service, we can easily add running nodes to serve more users(requests). Lightsail does not natively support auto-scaling features like other AWS services, such as EC2 or Fargate. But you can change capacity manually:
You can change
- The power of each node
- The number of each node
(surely that increasing power costs more money)
I send 100 requests in 5 seconds with Jmeter to the application running in 1 node and 3 nodes, you can see the response time is obviously shorter for 3 nodes:
# of Nodes | # of HTTP Requests | Average Response Time | Min | Max | Std. Dev. |
---|---|---|---|---|---|
1 | 100 | 3632 | 517 | 6417 | 1737.77 |
3 | 100 | 1407 | 219 | 3138 | 724.16 |
Identifying Individual Nodes with a Unique Application ID
After creating 3 nodes, each time you visit the public URL, you might be directed to one of the nodes. Normally, you can't differentiate these 3 nodes. If I generate an application ID and show it on a page, you can find that each page reloading might take you to a different node.
Troubleshooting
Deployment failed for exec format error
⁉️ Error in the server log
exec /docker-entrypoint.sh: exec format error
This error usually indicates that the file you're trying to execute is not in the correct format. In this case, it could mean that the entry point script you're trying to run in the Docker image (/docker-entrypoint.sh) is not in the correct format for the underlying system architecture.
Solution
Make sure the Docker image you're using is compatible with the system architecture of the Fargate cluster. If you're using a 64-bit Ubuntu 18.04 image, for example, the architecture should be x86_64.
Modify Dockerfile:
FROM amd64/nginx:1.23.3
⁉️ Exec format error
Error in the server log
[16/May/2023:08:37:49] [deployment:1] Creating your deployment
[16/May/2023:08:38:18] exec /start.sh: exec format error
[16/May/2023:08:39:28] [deployment:1] Started 1 new node
…
[16/May/2023:10:36:54] [deployment:2] Took too long
Root Cause
PostgreSQL is also architecture dependent.
Solution
Specify PostgreSQL with amd64 architecture:
RUN echo "deb [arch=amd64] http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" > /etc/apt/sources.list.d/pgdg.list
⁉️ Health checks failed: port 8080 is unhealthy
Error in the server log
[17/May/2023:07:51:30] [deployment:7] Health checks failed: port 8080 is unhealthy
Root cause
The default health check timeout of 2s is too short for my application.
Solution
Increase the timeout and interval and redeploy. Change the setting to 10s via the Lightsail console.
Source Code
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |